<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title></title>
<style>
body {
  background: #eee;
  margin-top: 50px;
}
svg {
	stroke: #000;
  width: 900px;
  display: block;
  margin: 0 auto;
}
svg text {
  font-family: "Noto Music", "Noto Sans", "Microsoft YaHei", sans-serif;
  font-size: 14px;
  letter-spacing: 0.1em;
}
svg text.clef {
  font-size: 48px;
}
svg text.note {
  font-size: 36px;
}
</style>
</head>
<body>
<script>
// 基础设施
function $svg (tag, attrs) {
	const node = document.createElementNS('http://www.w3.org/2000/svg', tag)
	for (const s in attrs)
		node.setAttribute(s, attrs[s])
	return node;
}

const $text = document.createTextNode.bind(document)

Text.prototype.appendTo =
Element.prototype.appendTo = function(parent) {
  parent.appendChild(this)
  return parent
}

Element.prototype.addClass = function(className) {
  this.classList.add(className)
  return this
}

// 全局变量
const scoreHeight = 500
const scoreWidth = 1000

// 一篇乐谱
class Score {
  // { id, width, height }
  constructor (opts) {
    for (const k in opts)
      this[k] = opts[k]
    this.svg = $svg('svg', {
      id: this.id,
      viewBox: `0 0 ${this.width} ${this.height}`
    })
  }
  show () {
    this.svg.appendTo(document.body)
  }
  appendChild (child) {
    return this.svg.appendChild(child)
  }
}

// 一行乐谱
class Staff {
  // { score, top, lines }
  constructor (opts) {
    for (const k in opts)
      this[k] = opts[k]
  }
  show () {
    for (let i = 0; i < this.lines; ++i) {
      const y = this.top + i * 10;
      $svg('line', {
        x1: 0,
        x2: this.score.width,
        y1: y,
        y2: y,
        stroke: '#888'
      }).appendTo(this.score)
    }
  }
}

// 音符
class Note {
  // { text, x, y }
  constructor (opts) {
    for (const k in opts)
      this[k] = opts[k]
    this.dict = {
      1: '\u{1d15d}',
      2: '\u{1d15e}',
      4: '\u{1d15f}',
      8: '\u{1d160}',
      16: '\u{1d161}',
      32: '\u{1d162}',
      64: '\u{1d163}'
    }
  }
  show () {
    const note = $svg('text', {
      x: this.x,
      y: this.y,
      //'text-anchor': 'middle',
      //'alignment-baseline': 'middle',
    })
    $text(this.dict[this.type]).appendTo(note).addClass('note').appendTo(this.score)
  }
}

// 谱号
class Clef {
  // { type, x, y }
  constructor (opts) {
    for (const k in opts)
      this[k] = opts[k]
    this.dict = {
      treble: '\u{1d11e}',
      bass: '\u{1d122}',
    }
  }
  show () {
    const clef = $svg('text', {
      x: this.x,
      y: this.y,
      //'text-anchor': 'middle',
      //'alignment-baseline': 'middle'
    })
    $text(this.dict[this.type]).appendTo(clef).addClass('clef').appendTo(this.score)
  }
}

function init(config) {
  const score = new Score(config.score)
  const classes = ['Staff', 'Clef', 'Note']
  classes.forEach(cls => {
    const constructor = eval(cls)
    const key = cls.toLowerCase()
    if (Array.isArray(config[key])) {
      config[key].forEach(obj => {
        new constructor({...obj, score}).show()
      })
    } else {
      new constructor({...config[key], score}).show()
    }
  })
  score.show()
}

init({
  score: {
    id: 'score',
    width: 1000,
    height: 500
  },
  staff: [
    { lines: 5, top: 30 },
    { lines: 5, top: 130 },
    { lines: 5, top: 230 },
  ],
  clef: [
    { type: 'treble', x: 15, y: 70 },
    { type: 'treble', x: 15, y: 170 },
    { type: 'treble', x: 15, y: 270 },
  ],
  note: [
    { type: '8', x: 80, y: 79 },
    { type: '8', x: 120, y: 74 },
    { type: '8', x: 160, y: 69 },
    { type: '8', x: 200, y: 64 },
    { type: '8', x: 240, y: 59 },
    { type: '4', x: 280, y: 54 },
  ]
})
</script>
</body>
</html>
